<?php
namespace Movie;

use BadRequestException;
use Selector;

/**
 * Class Douban
 * @package Movie
 */
class Douban extends AbstractMovie
{
    // 豆瓣基路径
    const doubanMovieBase = 'https://movie.douban.com';

    /**
     * 豆瓣url定义
     * 官网：https://www.douban.com
     */
    const doubanUrl = array(
        'moviePage'  => array(
            'subject'       => '/subject/{id}/',                // 电影条目信息
            'celebrities'   => '/subject/{id}/celebrities',     // 电影条目全部演职员
            's_photos'      => '/subject/{id}/photos?type=S',   // 电影条目剧照
            'r_photos'      => '/subject/{id}/photos?type=R',   // 电影条目海报
            'w_photos'      => '/subject/{id}/photos?type=W',   // 电影条目壁纸
            'all_photos'    => '/subject/{id}/all_photos',      // 电影条目全部图片
            'reviews'       => '/subject/{id}/reviews',         // 电影条目影评列表
            'comments'      => '/subject/{id}/comments',        // 电影条目短评列表
            'trailer'       => '/subject/{id}/trailer',         // 电影条目预告片
            'awards'        => '/subject/{id}/awards/',          // 电影条目获奖情况
            'search'   =>  'https://search.douban.com/movie/subject_search?search_text={text}&cat=1002',    // 豆瓣页面搜索
            'recommendations' => array('url' => 'moviePage.subject', 'type' => 'css', 'selector' => '#recommendations'),        // 喜欢这部电影的人也喜欢 id=recommendations
            'celebrity' => '/celebrity/{id}/',          // 影人条目信息
            'works'     => '/celebrity/{id}/movies',    // 影人作品?sortby=vote&format=pic&role=【参数详解：sortby可选time、vote，format可选pic、text】
            'c_photos'  => '/celebrity/{id}/photos',    // 影人剧照
            'c_awards'  => '/celebrity/{id}/awards/',   // 影人获奖情况
            'partners'  => '/celebrity/{id}/partners',  // 合作2次以上的影人
            'top250'    => '/top250',       // Top250
            'chart'     => '/chart',        // 豆瓣电影排行榜
            'Oscar'     => '/awards/Oscar/{text}/'  // 奥斯卡金像奖.USA http://oscar.go.com
        ),
    );

    /**
     * 父类的初始化回调，子类重写此方法
     */
    public function onInit()
    {
        // 设置超时
        ini_set('max_execution_time', 30);
        set_time_limit(30);
    }

    /**
     * 豆瓣电影条目页面 解码方法，抽象方法必须实现
     * @param int $id 电影条目id
     * @return array|bool
     * @throws BadRequestException
     */
    public function SubjectPage($id = 0)
    {
        if (empty($id)) {
            return null;
        }
        // 页面选择器
        $selector = [
            'json' => '//script[@type="application/ld+json"]',    // JSON-LD结构化数据
            'year'  => '//span[@class="year"]',     // 奖项年份
            'title'    => '//title',                // 中文名
            'original_title' => '//span[@property="v:itemreviewed"]',   // 原名
            'summary' => '//span[@property="v:summary"]',   // 剧情简介
            'info'  => '//div[@id="info"]',             // 影视信息
            'genres' => '//span[@property="v:genre"]',  // 影片类型
            'pubdates' => '//span[@property="v:initialReleaseDate"]',  // 上映首映日期
            'durations' => '//span[@property="v:runtime"]',  // 片长
            'countries' => '制片国家/地区:',
            'languages' => '语言:',
            'aka'       => '又名:',
            'episodes_count'=> '集数:',   // 剧集特有
            'duration'  =>  '单集片长:',    // 剧集特有
            'mainpic'   =>  '//div[@id="mainpic"]',     // 海报容器
            'images'    =>  '//img[@rel="v:image"]',
            "average"   => '//strong[@property="v:average"]',   // 评分
            "numRaters" => '//span[@property="v:votes"]',       // 评分人数
            'tags' => '//div[@class="tags-body"]/a',            // 标签
            'href'  => '//a/@href',
            'a'     => '//a'
        ];
        /**
         * 1.检查文件缓存是否存在
         */
        $cacheKey = ['Model', 'Douban', $id];
        /**
         * 调试模式，不查缓存
         */
        if ($res = $this->getFileCache($cacheKey)) {
            return $res;
        }
        /**
         * 2.请求电影条目页面
         */
        $subjectUrl = str_replace('{id}', $id, self::doubanUrl['moviePage']['subject']);
        $html = $this->request(self::doubanMovieBase . $subjectUrl, [], false);
        // 搜索特征码，加强检查
        if (empty($html) || strpos($html, '/subject/'.$id) === false) {
            throw new BadRequestException($id.'的影视条目不存在！', 404);
        }
        /**
         * 正常流程
         */
        // 返回的数据结构
        $rs = self::Subject;
        $rs['id'] = intval($id);
        $rs['@form'] = 'douban';
        $rs['alt'] = self::doubanMovieBase . $subjectUrl;
        // 年份
        $_year = Selector::select($html, $selector['year']);
        if (preg_match(self::PrimaryRegex['d'], $_year, $matches)) {
            $rs['year'] = $matches[0];
        }
        // 中文名
        $_title = Selector::select($html, $selector['title']);
        if (!empty($_title)) {
            $rs['title'] = trim(str_replace('(豆瓣)', '', $_title));
        }
        // 页面结构化信息
        $arrayLD = $this->getJsonLD($html);
        if ($arrayLD) {
            // 页面解码特有的 @type
            if (!empty($arrayLD['@type'])) {
                $rs['@type'] =  strtolower($arrayLD['@type']);
                $rs['subtype'] = strtolower($arrayLD['@type']);
            } else {
                unset($rs['@type']);
            }
            // 持续时间 格式：PnYnMnDTnHnMnS 例如：PT1H59M
            if (!empty($arrayLD['duration'])) {
                if (preg_match(self::PrimaryRegex['duration'], $arrayLD['duration'], $matchs)) {
                    // 转换 秒
                    $seconds = $this->ISO8601_to_seconds($arrayLD['duration']);
                    if ($seconds) {
                        $rs['durations'] = array($seconds);
                    }
                }
            }
            $rs['rating']['max'] = $arrayLD['aggregateRating']['bestRating'] ?? '';
            $rs['rating']['average'] = $arrayLD['aggregateRating']['ratingValue'] ?? '';
            $rs['rating']['numRaters'] = $arrayLD['aggregateRating']['ratingCount'] ?? '';
            $rs['rating']['min'] = $arrayLD['aggregateRating']['worstRating'] ?? '';
            $_celebrities = $this->getCelebrityByJsonLD($arrayLD);
            $rs = array_merge($rs, $_celebrities);
            // 原名
            if (isset($arrayLD['name'])) {
                $rs['original_title'] = trim(str_replace($rs['title'], '', $arrayLD['name']));
            }
            $rs['JsonLD'] = $arrayLD;
        } else {
            // 原名
            $_original_title = Selector::select($html, $selector['original_title']);
            $rs['original_title'] = !empty($_original_title) ? trim(str_replace($rs['title'], '', $_original_title)) : '';
        }

        // 全部演职员
        $_celebrities = $this->celebritiesPage($id);
        if (!empty($_celebrities)) {
            $rs = array_merge($rs, $_celebrities);
        }
        // 通用：标签tags
        $_tags = Selector::select($html, $selector['tags']);
        if (!empty($_tags)) {
            $rs['tags'] = is_array($_tags) ? $_tags : [$_tags];
        }
        // 剧情简介
        $_summary = trim(Selector::select($html, $selector['summary']));
        if (!empty($_summary)) {
            $_summary = str_replace(["\n", "\t", "　",'©豆瓣'], '', $_summary);
            $rs['summary'] = str_replace('<br/>', "\n", $_summary);
        }
        // 海报
        $mainpic = Selector::select($html, $selector['mainpic']);
        $images = Selector::select($mainpic, $selector['images']);
        if (!empty($images)) {
            $rs['images']['small'] = $images;
            $images = str_replace('img3.doubanio.com', 'img1.doubanio.com', $images);
            $large  = str_replace('/s_ratio_poster', '/l_ratio_poster', $images);
            $medium = str_replace('/s_ratio_poster', '/m_ratio_poster', $images);
            $rs['images']['large'] = $large;
            $rs['images']['medium'] = $medium;
        }

        // 主要信息
        $info = Selector::select($html, $selector['info']);
        if (!empty($info)) {
            $_info = explode('<br/>', $info);
            if (count($_info) > 3) {
                foreach ($_info as $k => $item) {
                    $item = trim(strip_tags($item));  // 过滤html
                    // 前置处理
                    $key = '';
                    if (strpos($item, $selector['countries']) !== false) {
                        // 制片国家/地区
                        $key = 'countries';
                    } elseif (strpos($item, $selector['languages']) !== false) {
                        // 语言
                        $key = 'languages';
                    } elseif (strpos($item, $selector['aka']) !== false) {
                        // 又名
                        $key = 'aka';
                    } elseif (strpos($item, $selector['episodes_count']) !== false) {
                        // 集数
                        $key = 'episodes_count';
                    } elseif (strpos($item, $selector['duration']) !== false) {
                        // 单集片长
                        $value = str_replace([$selector['duration'], ' '], '', $item);
                        $rs['durations'] = explode('/', $value);
                    } else {
                        continue;
                    }

                    // 后置处理
                    if ($key != '') {
                        // 过滤标题
                        $value = str_replace($selector[$key], '', $item);
                        // 转换为数组
                        $rs[$key] = explode('/', $value);
                        // 遍历数组，过滤空格
                        array_walk($rs[$key], function (&$v, $k) {
                            $v = trim($v);
                        });
                        $rs[$key] = array_filter($rs[$key], function ($v, $k){
                            return !empty($v);
                        } ,ARRAY_FILTER_USE_BOTH);
                    }
                }
            }

            // 通用：IMDb
            if (preg_match(self::PrimaryRegex['tt_d'], $info, $matches)) {
                $rs['imdb'] = $matches[0];
            }
            // 通用：类型
            $genres = Selector::select($info, $selector['genres']);
            if (!empty($genres)) {
                $rs['genres'] = is_array($genres) ? $genres : [$genres];
            }
            // 通用：上映日期
            $pubdates = Selector::select($info, $selector['pubdates']);
            if (!empty($pubdates)) {
                $rs['pubdates'] = is_array($pubdates) ? $pubdates : [$pubdates];
            }
            // 片长
            $durations = Selector::select($info, $selector['durations']);
            if (!empty($durations)) {
                $rs['durations'] = is_array($durations) ? $durations : [$durations];
            }
        }

        // 获奖情况
        $_awards = $this->SubjectAwardsPage($id);
        if (!empty($_awards['awards'])) {
            $rs['awards'] = $_awards['awards'];
        }

        // 设置缓存
        $this->setFileCache($cacheKey, $rs, $this->cacheExpire * 5);        // 文件缓存35天
        return $rs;
    }

    /**
     * 从豆瓣的结构化数据内，提取演职员信息
     * @param array $arrayLD
     * @return array
     */
    public function getCelebrityByJsonLD(array $arrayLD = []):array
    {
        $rs = [];
        // 字段映射：页面特征 => 数据最终键名
        $list_map = ['director' => 'directors', 'author' => 'writers', 'actor' => 'casts'];
        foreach ($list_map as $key => $item) {
            $value = $this->formatCelebrityInJsonLD($arrayLD[$key]);
            if (!empty($value)) {
                $rs[$item] = $value;
            }
        }
        return $rs;
    }

    /**
     * 豆瓣结构化数据内的影人信息转换成标准格式
     * @param $celebrity
     * @return array|bool
     */
    protected function formatCelebrityInJsonLD($celebrity)
    {
        if (!empty($celebrity) && is_array($celebrity)) {
            foreach ($celebrity as $key => &$item) {
                // @type值：Person
                if (isset($item['@type']) && $item['@type']) {
                    $item['@type'] = strtolower($item['@type']);
                    if (preg_match(self::PrimaryRegex['d'], $item['url'], $matches)) {
                        $item['id'] = $matches[0];
                        $item['alt'] = self::doubanMovieBase . $item['url'];
                        unset($item['url']);
                    }
                }
            }
            return $celebrity;
        }
        return false;
    }

    /**
     * 从影人页面，提取豆瓣电影条目全部演职员
     * - 包含：演员id、演员名字、演员头像、饰演角色、演员代表作
     * @param int $id    电影条目id
     * @return array|bool
     */
    public function celebritiesPage(int $id = 0)
    {
        // 页面选择器
        $selector = [
            'celebrities' => '//div[@id="celebrities"]',    // 全部演职员容器
            'list'  => '//div[@class="list-wrapper"]',      // 导演、主演、编剧、制片人容器
            'h2'    => '//h2',                              // 标题
        ];
        // 返回的数据结构
        $rs = [
            'id' => $id,
            // 电影条目页 URL
            'alt' => self::doubanMovieBase . str_replace('{id}', $id, self::doubanUrl['moviePage']['subject']),
            // 导演director
            'directors' => [],
            // 主演actor
            'casts' => [],
            // 编剧author
            'writers' => [],
            '@form'   => 'douban',
        ];
        /**
         * 1.检查文件缓存是否存在
         */
        $cacheKey = ['Model', 'DoubanCelebrities', $id];
        /**
         * 调试模式，不查缓存
         */
        if ($res = $this->getFileCache($cacheKey)) {
            return $res;
        }
        /**
         * 2.请求全部演职员页面
         */
        // 请求全部演职员页面
        $celebritiesUrl = str_replace('{id}', $id, self::doubanUrl['moviePage']['celebrities']);
        $html = $this->request(self::doubanMovieBase . $celebritiesUrl, [], false);
        // 搜索特征码，加强检查
        if (empty($html) || strpos($html, '/subject/'.$id) === false) {
            return false;
        }
        /**
         * 正常流程
         */
        // 父容器
        $celebrities = Selector::select($html, $selector['celebrities']);
        if (empty($celebrities)) {
            return false;
        }
        // 子容器（循环提取的列表）
        $list_wrapper = Selector::select($celebrities, $selector['list']);
        if (empty($list_wrapper)) {
            return false;
        }
        $list_wrapper = is_string($list_wrapper) ? [$list_wrapper] : $list_wrapper;
        /**
         * 提取：导演Director、演员Cast、编剧Writer、制片人Producer
         */
        // 字段映射：页面特征 => 数据最终键名
        $h2_map = ['Director' => 'directors', 'Cast' => 'casts', 'Writer' => 'writers'];
        $default_key = 'casts';     // 默认值，增强兼容性
        // 遍历：导演、主演、编剧
        foreach ($list_wrapper as $k => $celebrities_html) {
            /**
             * 循环比较，计算键名 $key 的值
             */
            $h2 = Selector::select($celebrities_html, $selector['h2']);    // 当前特征的标题
            if (empty($h2)) {
                continue;
            }
            $key = $default_key;     // 默认值，增强兼容性
            if (!empty($h2_map)) {
                foreach ($h2_map as $that => $value) {
                    // 不区分大小写查找
                    if (stripos($h2, $that) !== false) {
                        $key = $value;
                        unset($h2_map[$that]);
                        break;
                    }
                }
            }
            /**
             * 获取演职员列表，结果入栈（$rs[$key]传址）
             */
            $this->decodeCelebrityIntact($celebrities_html, $rs[$key]);
        }

        // 设置缓存
        $this->setFileCache($cacheKey, $rs, $this->cacheExpire * 5);        // 文件缓存35天

        return $rs;
    }

    /**
     * 获取演职员列表（完整）
     * 从选择器 //li[@class="celebrity"] 中提取
     * - 包含：演员id、演员名字、演员头像、饰演角色、演员代表作
     * @param string $html
     * @param array $rs
     */
    private function decodeCelebrityIntact(string $html = '', array &$rs = [])
    {
        if (empty($html)) {
            return;
        }
        /**
         * 获取演职员列表（完整信息）
         * - 包含演员id、演员名字、演员头像、演员饰演、演员代表作
         */
        // 页面选择器
        $selector = [
            'celebrity' => '//li[@class="celebrity"]',  // 演职员信息 （包含演员id、演员名字、演员头像、演员饰演、演员代表作）
            'info'      => '//div[@class="info"]',      // 演职员信息 （包含演员id、演员名字、演员头像、演员饰演、演员代表作）
            'spanName'  => '//span[@class="name"]',         // 演职员信息.精简 （包含演员id、演员名字）
            'alt'   => '//a/@href',                         // 演职员超链接
            'name'  => '//a',                               // 演职员名字
            'avatar'=> '/url\((?P<src>.*?)\)/',             // 演职员头像 （正则表达式）
            'celebrity_default_avatar' => 'celebrity-default',  // 演职员头像为空时，豆瓣的默认值 特征
            'role'  =>  '//span[@class="role"]',             // 演职员饰演角色
            'works' =>  '//span[@class="works"]//@href',     // 演职员代表作(获取的是超链接)
        ];
        $li_celebrity = Selector::select($html, $selector['celebrity']);    // 演职员容器，完整
        if (empty($li_celebrity)) {
            return;
        }
        //如果是字符串，则转数组
        if (is_string($li_celebrity)) {
            $li_celebrity = [$li_celebrity];
        }
        foreach ($li_celebrity as $li) {
            /**
             * 演职员数据结构
             */
            $celebrity = self::Celebrity;
            $spanName = Selector::select($li, $selector['spanName']);       // 演职员信息
            if (!empty($spanName) && is_string($spanName)) {
                $alt = Selector::select($spanName, $selector['alt']);       // 演员超链接
                if (!empty($alt) && is_string($alt))
                {
                    // 提取演员数字id
                    $matches = [];  // INIT
                    if (preg_match(self::PrimaryRegex['d'], $alt, $matches)) {
                        // 个人页
                        $celebrity['alt'] = $alt;

                        // ID
                        $celebrity['id'] = $matches[0];

                        // 名字
                        $name = Selector::select($spanName, $selector['name']);
                        if (!empty($name) && is_string($name)) {
                            $celebrity['name'] = $name;
                        }

                        // 头像
                        $avatar_matches = [];  // INIT
                        if (preg_match($selector['avatar'], $li, $avatar_matches)) {
                            $avatar = $avatar_matches['src'];
                            // 影人头像默认值：celebrity-default-medium
                            if (!empty($avatar) && strpos($avatar, $selector['celebrity_default_avatar']) === false) {
                                $celebrity['avatar'] = $avatar;
                            }
                        }

                        // 饰演角色
                        $role = Selector::select($li, $selector['role']);
                        if (!empty($role) && is_string($role)) {
                            $celebrity['role'] = $role;
                        }

                        // 代表作
                        $works_a = Selector::select($li, $selector['works']);
                        if (!empty($works_a)) {
                            if (is_string($works_a)) {
                                $works_a = [$works_a];
                            }
                            $celebrity['works'] = $works_a;
                        }

                        /**
                         * 结果入栈
                         */
                        $rs[] = $celebrity;
                    }
                }
            }
        }
    }

    /**
     * 豆瓣电影条目获奖情况 解码方法
     * @param int $id    电影条目id
     * @return array|bool
     */
    public function SubjectAwardsPage(int $id = 0)
    {
        // 获奖情况页面选择器
        $selector = [
            'article'  => '//div[@class="article"]',    // 全部获奖情况容器
            'awards'   => '//div[@class="awards"]',     // 各奖项容器
            'h2'    => '//h2',                      // 奖项名
            'year'  => '//span[@class="year"]',     // 奖项年份
            'award' => '//ul[@class="award"]',      // 奖项详情
            'li'    => '//li',                      // 奖项条目
            'url'   => '//a/@href',     // 通用
            'a'     => '//a'            // 通用
        ];
        // 返回的数据结构
        $rs = [
            'id' => $id,
            'awards' => [],
            '@form'  => 'douban',
        ];

        // 请求获奖情况页面
        $awardsUrl = str_replace('{id}', $id, self::doubanUrl['moviePage']['awards']);
        $html = $this->request(self::doubanMovieBase . $awardsUrl, [], false);
        // 搜索特征码，加强检查
        if (empty($html) || strpos($html, '/subject/'.$id) === false) {
            return false;
        }
        /**
         * 正常流程
         */
        $article = Selector::select($html, $selector['article']);
        $_awards = Selector::select($article, $selector['awards']);
        if (empty($article) || empty($_awards)) {
            // 暂未获奖
            return false;
        }
        if (is_string($_awards)) {
            $_awards = [$_awards];
        }
        // 1层 遍历：各奖项列表
        foreach ($_awards as $k => $v) {
            // 获奖数据结构
            $awards = [
                'title' => '',
                'url'   => '',
                'year'    => 0
            ];

            $h2 = Selector::select($v, $selector['h2']);
            $awards['title'] = Selector::select($h2, $selector['a']);    // 奖项名
            $awards['url'] = Selector::select($h2, $selector['url']);    // 奖项URL
            $year = Selector::select($h2, $selector['year']);
            $matches = [];
            if ($year && preg_match(self::PrimaryRegex['d'], $year, $matches)) {
                $awards['year'] = $matches[0];                               // 奖项年份
            }
            // 2层 奖项获奖详情
            $awardList = Selector::select($v, $selector['award']);
            if (is_string($awardList)) {
                $awardList = [$awardList];
            }
            foreach ($awardList as $key => $value) {
                // 3层
                $award = [
                    'name' => ''
                ];
                $liList = Selector::select($value, $selector['li']);
                if (is_array($liList)) {
                    $award['name'] = $liList[0];      // 获奖名
                    unset($liList[0]);     // 移除使用过的元素
                    // 遍历：获奖详情
                    foreach ($liList as $item) {
                        if (empty($item) || $item == '<li/>') {
                            continue;
                        }
                        // 提名
                        if (stripos($item, '</a>') != false) {
                            // 正则表达式提取所有a标签
                            $reg = "/<a [^>]*>.*<\/a>/";
                            $a_List = [];
                            if (empty(preg_match_all($reg, $item, $a_List))) {
                                // 如果返回0或者false，直接返回
                                continue;
                            }
                            $aList = $a_List[0];
                            if (is_string($aList)) {
                                $aList = [$aList];
                            }
                            //print_r($aList);
                            //continue;
                            foreach ($aList as $iitem) {
                                if (empty($iitem) && stripos($iitem, '</a>') === false) {
                                    continue;
                                }
                                // 演员数据结构
                                $celebrity = [
                                    'alt'   => '',
                                    'id'    => 0,
                                    'name'  => ''
                                ];
                                $alt = Selector::select($iitem, $selector['url']);
                                if (!empty($alt) && is_string($alt)) {
                                    $celebrity['alt'] = $alt;
                                    $matches = [];
                                    if (preg_match(self::PrimaryRegex['d'], $celebrity['alt'], $matches)) {
                                        $celebrity['id'] = $matches[0];
                                        $name = Selector::select($iitem, $selector['a']);
                                        if (!empty($name) && is_string($name)) {
                                            $celebrity['name'] = $name;
                                        }
                                        $award['celebrity'][] = $celebrity;
                                    }
                                }
                            }
                        }
                    }
                } else {
                    $award['name'] = $liList;
                }
                $awards['award'][] = $award;
                // 3层结束
            } // 2层 结束
            $rs['awards'][] = $awards;
        } // 1层 结束
        return $rs;
    }
}
